from collections import OrderedDict, defaultdict
from datetime import date, datetime, time
from dateutil.relativedelta import relativedelta
from functools import partial
from operator import attrgetter
import itertools
import logging
import base64

import pytz

from odoo.tools import pycompat, date_utils

try:
    from xmlrpc.client import MAXINT
except ImportError:
    pass

import psycopg2

from .sql_db import LazyCursor
from .tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar
from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
from .tools.translate import html_translate, _
from .tools.mimetypes import guess_mimetype

DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
EMPTY_DICT = frozendict()

RENAMED_ATTRS = [('select', 'index'), ('digits_compute', 'digits')]

_logger = logging.getLogger(__name__)
_schema = logging.getLogger(__name__[:-7] + '.schema')

Default = object()

def copy_cache(records, env):
    pass

def first(records):
    pass

def resolve_mro(model, name, predicate):
    pass

class MetaField(type):
    by_type = {}

    def __new__(meta, name, bases, attrs):
        base_slots = {}
        for base in reversed(bases):
            base_slots.update(getattr(base, '_slots', ()))
        slots = dict(base_slots)
        slots.update(attrs.get('_slots', ()))

        attrs['__slots__'] = set(slots) - set(base_slots)
        attrs['_slots'] = slots
        return type.__new__(meta, name, bases, attrs)

    def __init__(cls, name, bases, attrs):
        super(MetaField, cls).__init__(name, bases, attrs)
        if not hasattr(cls, 'type'):
            return
        if cls.type and cls.type not in MetaField.by_type:
            MetaField.by_type[cls.type] = cls

        # compute class attributes to avoid calling dir() on fields
        cls.related_attrs = []
        cls.description_attrs = []
        for attr in dir(cls):
            if attr.startswith('_related_'):
                cls.related_attrs.append((attr[9:], attr))
            elif attr.startswith('_description_'):
                cls.description_attrs.append((attr[13:], attr))

_global_seq = iter(itertools.count())

class Field(MetaField('DummyField', (object,), {})):
    type = None  # type of the field (string)
    relational = False  # whether the field is a relational one
    translate = False  # whether the field is translated

    column_type = None  # database column type (ident, spec)
    column_format = '%s'  # placeholder for value in queries
    column_cast_from = ()  # column types that may be cast to this

    _slots = {
        'args': EMPTY_DICT,  # the parameters given to __init__()
        '_attrs': EMPTY_DICT,  # the field's non-slot attributes
        '_module': None,  # the field's module name
        '_modules': None,  # modules that define this field
        '_setup_done': None,  # the field's setup state: None, 'base' or 'full'
        '_sequence': None,  # absolute ordering of the field

        'automatic': False,  # whether the field is automatically created ("magic" field)
        'inherited': False,  # whether the field is inherited (_inherits)
        'inherited_field': None,  # the corresponding inherited field

        'name': None,  # name of the field
        'model_name': None,  # name of the model of this field
        'comodel_name': None,  # name of the model of values (if relational)

        'store': True,  # whether the field is stored in database
        'index': False,  # whether the field is indexed in database
        'manual': False,  # whether the field is a custom field
        'copy': True,  # whether the field is copied over by BaseModel.copy()
        'depends': None,  # collection of field dependencies
        'recursive': False,  # whether self depends on itself
        'compute': None,  # compute(recs) computes field on recs
        'compute_sudo': False,  # whether field should be recomputed as admin
        'inverse': None,  # inverse(recs) inverses field on recs
        'search': None,  # search(recs, operator, value) searches on self
        'related': None,  # sequence of field names, for related fields
        'related_sudo': True,  # whether related fields should be read as admin
        'company_dependent': False,  # whether ``self`` is company-dependent (property field)
        'default': None,  # default(recs) returns the default value

        'string': None,  # field label
        'help': None,  # field tooltip
        'readonly': False,  # whether the field is readonly
        'required': False,  # whether the field is required
        'states': None,  # set readonly and required depending on state
        'groups': None,  # csv list of group xml ids
        'change_default': False,  # whether the field may trigger a "user-onchange"
        'deprecated': None,  # whether the field is deprecated

        'related_field': None,  # corresponding related field
        'group_operator': None,  # operator for aggregating values
        'group_expand': None,  # name of method to expand groups in read_group()
        'prefetch': True,  # whether the field is prefetched
        'context_dependent': False,  # whether the field's value depends on context
    }
    def __init__(self, string=Default, **kwargs):
        kwargs['string'] = string
        self._sequence = kwargs['_sequence'] = next(_global_seq)
        args = {key: val for key, val in kwargs.items() if val is not Default}
        self.args = args or EMPTY_DICT
        self._setup_done = None

    def new(self, **kwargs):
        pass

    def __getattr__(self, name):
        pass

    def __setattr__(self, name, value):
        try:
            object.__setattr__(self, name, value)
        except AttributeError:
            pass

    def set_all_attrs(self, attrs):
        pass

    def __delattr__(self, name):
        pass

    def __str__(self):
        pass

    def __repr__(self):
        pass

    def setup_base(self, model, name):
        pass

    def _can_setup_from(self, field):
        pass

    def _get_attrs(self, model, name):
        pass

    def _setup_attrs(self, model, name):
        pass

    def setup_full(self, model):
        pass

    def _setup_regular_base(self, model):
        pass

    def _setup_regular_full(self, model):
        pass

    def _setup_related_full(self, model):
        pass

    def traverse_related(self, record):
        pass

    def _compute_related(self, records):
        pass

    def _inverse_related(self, records):
        pass

    def _search_related(self, records, operator, value):
        pass
    _related_comodel_name = property(attrgetter('comodel_name'))
    _related_string = property(attrgetter('string'))
    _related_help = property(attrgetter('help'))
    _related_groups = property(attrgetter('groups'))
    _related_group_operator = property(attrgetter('group_operator'))
    _related_context_dependent = property(attrgetter('context_dependent'))

    @property
    def base_field(self):
        pass

    def _default_company_dependent(self, model):
        pass

    def _compute_company_dependent(self, records):
        pass

    def _inverse_company_dependent(self, records):
        pass

    def _search_company_dependent(self, records, operator, value):
        pass

    def resolve_deps(self, model, path0=[], seen=frozenset()):
        pass

    def setup_triggers(self, model):
        pass

    def get_description(self, env):
        pass

    _description_store = property(attrgetter('store'))
    _description_manual = property(attrgetter('manual'))
    _description_depends = property(attrgetter('depends'))
    _description_related = property(attrgetter('related'))
    _description_company_dependent = property(attrgetter('company_dependent'))
    _description_readonly = property(attrgetter('readonly'))
    _description_required = property(attrgetter('required'))
    _description_states = property(attrgetter('states'))
    _description_groups = property(attrgetter('groups'))
    _description_change_default = property(attrgetter('change_default'))
    _description_deprecated = property(attrgetter('deprecated'))
    _description_group_operator = property(attrgetter('group_operator'))

    @property
    def _description_searchable(self):
        pass

    @property
    def _description_sortable(self):
        pass

    def _description_string(self, env):
        pass

    def _description_help(self, env):
        pass

    def null(self, record):
        pass

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_record(self, value, record):
        pass

    def convert_to_read(self, value, record, use_name_get=True):
        pass

    def convert_to_write(self, value, record):
        pass

    def convert_to_onchange(self, value, record, names):
        pass

    def convert_to_export(self, value, record):
        pass

    def convert_to_display_name(self, value, record):
        pass

    def update_db(self, model, columns):
        pass

    def update_db_column(self, model, column):
        pass

    def update_db_notnull(self, model, column):
        pass

    def update_db_index(self, model, column):
        pass

    def update_db_related(self, model):
        pass

    def read(self, records):
        pass

    def create(self, record_values):
        pass

    def write(self, records, value):
        pass

    def __get__(self, record, owner):
        pass

    def __set__(self, record, value):
        pass

    def _compute_value(self, records):
        pass

    def compute_value(self, records):
        pass

    def determine_value(self, record):
        pass

    def determine_draft_value(self, record):
        pass

    def determine_inverse(self, records):
        pass

    def determine_domain(self, records, operator, value):
        pass

    def modified_draft(self, records):
        pass

class Boolean(Field):
    type = 'boolean'
    column_type = ('bool', 'bool')

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_export(self, value, record):
        pass



class Integer(Field):
    type = 'integer'
    column_type = ('int4', 'int4')
    _slots = {
        'group_operator': 'sum',
    }

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_read(self, value, record, use_name_get=True):
        pass

    def _update(self, records, value):
        pass

    def convert_to_export(self, value, record):
        pass

class Float(Field):
    type = 'float'
    column_cast_from = ('int4', 'numeric', 'float8')
    _slots = {
        '_digits': None,  # digits argument passed to class initializer
        'group_operator': 'sum',
    }

    def __init__(self, string=Default, digits=Default, **kwargs):
        pass

    @property
    def column_type(self):
        pass

    @property
    def digits(self):
        pass

    _related__digits = property(attrgetter('_digits'))
    _description_digits = property(attrgetter('digits'))

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_export(self, value, record):
        pass

class Monetary(Field):
    type = 'monetary'
    column_type = ('numeric', 'numeric')
    column_cast_from = ('float8',)
    _slots = {
        'currency_field': None,
        'group_operator': 'sum',
    }

    def __init__(self, string=Default, currency_field=Default, **kwargs):
        pass

    _description_currency_field = property(attrgetter('currency_field'))

    def _setup_currency_field(self, model):
        pass

    def _setup_regular_full(self, model):
        pass

    def _setup_related_full(self, model):
        pass

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_read(self, value, record, use_name_get=True):
        pass

    def convert_to_write(self, value, record):
        pass

class _String(Field):
    _slots = {
        'translate': False,  # whether the field is translated
        'prefetch': None,
    }

    def __init__(self, string=Default, **kwargs):
        if 'translate' in kwargs and not callable(kwargs['translate']):
            kwargs['translate'] = bool(kwargs['translate'])
        super(_String, self).__init__(string=string, **kwargs)

    def _setup_attrs(self, model, name):
        pass

    _related_translate = property(attrgetter('translate'))

    def _description_translate(self, env):
        pass

    def get_trans_terms(self, value):
        pass

    def get_trans_func(self, records):
        pass

    def check_trans_value(self, value):
        pass

class Char(_String):
    type = 'char'
    column_cast_from = ('text',)
    _slots = {
        'size': None,  # maximum size of values (deprecated)
        'trim': True,  # whether value is trimmed (only by web client)
    }

    @property
    def column_type(self):
        pass

    def update_db_column(self, model, column):
        pass

    _related_size = property(attrgetter('size'))
    _related_trim = property(attrgetter('trim'))
    _description_size = property(attrgetter('size'))
    _description_trim = property(attrgetter('trim'))

    def _setup_regular_base(self, model):
        pass

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

class Text(_String):
    type = 'text'
    column_type = ('text', 'text')
    column_cast_from = ('varchar',)

    def convert_to_cache(self, value, record, validate=True):
        pass

class Html(_String):
    type = 'html'
    column_type = ('text', 'text')
    _slots = {
        'sanitize': True,  # whether value must be sanitized
        'sanitize_tags': True,  # whether to sanitize tags (only a white list of attributes is accepted)
        'sanitize_attributes': True,  # whether to sanitize attributes (only a white list of attributes is accepted)
        'sanitize_style': False,  # whether to sanitize style attributes
        'strip_style': False,  # whether to strip style attributes (removed and therefore not sanitized)
        'strip_classes': False,  # whether to strip classes attributes
    }

    def _get_attrs(self, model, name):
        pass

    _related_sanitize = property(attrgetter('sanitize'))
    _related_sanitize_tags = property(attrgetter('sanitize_tags'))
    _related_sanitize_attributes = property(attrgetter('sanitize_attributes'))
    _related_sanitize_style = property(attrgetter('sanitize_style'))
    _related_strip_style = property(attrgetter('strip_style'))
    _related_strip_classes = property(attrgetter('strip_classes'))

    _description_sanitize = property(attrgetter('sanitize'))
    _description_sanitize_tags = property(attrgetter('sanitize_tags'))
    _description_sanitize_attributes = property(attrgetter('sanitize_attributes'))
    _description_sanitize_style = property(attrgetter('sanitize_style'))
    _description_strip_style = property(attrgetter('strip_style'))
    _description_strip_classes = property(attrgetter('strip_classes'))

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

class Date(Field):
    type = 'date'
    column_type = ('date', 'date')
    column_cast_from = ('timestamp',)

    start_of = staticmethod(date_utils.start_of)
    end_of = staticmethod(date_utils.end_of)
    add = staticmethod(date_utils.add)
    subtract = staticmethod(date_utils.subtract)

    @staticmethod
    def today(*args):
        pass

    @staticmethod
    def context_today(record, timestamp=None):
        pass

    @staticmethod
    def to_date(value):
        pass

    from_string = to_date

    @staticmethod
    def to_string(value):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_export(self, value, record):
        pass
        

class Datetime(Field):
    type = 'datetime'
    column_type = ('timestamp', 'timestamp')
    column_cast_from = ('date',)

    start_of = staticmethod(date_utils.start_of)
    end_of = staticmethod(date_utils.end_of)
    add = staticmethod(date_utils.add)
    subtract = staticmethod(date_utils.subtract)

    @staticmethod
    def now(*args):
        pass

    @staticmethod
    def today(*args):
        pass

    @staticmethod
    def context_timestamp(record, timestamp):
        pass

    @staticmethod
    def to_datetime(value):
        pass

    from_string = to_datetime

    @staticmethod
    def to_string(value):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_export(self, value, record):
        pass

    def convert_to_display_name(self, value, record):
        pass


_BINARY = memoryview
if pycompat.PY2:
    _BINARY = buffer

class Binary(Field):
    type = 'binary'
    _slots = {
        'prefetch': False,  # not prefetched by default
        'context_dependent': True,  # depends on context (content or size)
        'attachment': False,  # whether value is stored in attachment
    }

    @property
    def column_type(self):
        pass

    def _get_attrs(self, model, name):
        pass

    _description_attachment = property(attrgetter('attachment'))

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def read(self, records):
        pass

    def create(self, record_values):
        pass

    def write(self, records, value):
        pass

class Selection(Field):
    type = 'selection'
    _slots = {
        'selection': None,  # [(value, string), ...], function or method name
        'validate': True,  # whether validating upon write
    }

    def __init__(self, selection=Default, string=Default, **kwargs):
        super(Selection, self).__init__(selection=selection, string=string, **kwargs)

    @property
    def column_type(self):
        pass

    def _setup_regular_base(self, model):
        pass

    def _setup_related_full(self, model):
        pass

    def _setup_attrs(self, model, name):
        pass

    def _description_selection(self, env):
        pass

    def get_values(self, env):
        pass

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_export(self, value, record):
        pass

class Reference(Selection):
    type = 'reference'

    @property
    def column_type(self):
        pass

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_record(self, value, record):
        pass

    def convert_to_read(self, value, record, use_name_get=True):
        pass

    def convert_to_export(self, value, record):
        pass

    def convert_to_display_name(self, value, record):
        pass

class _Relational(Field):
    relational = True
    _slots = {
        'domain': [],  # domain for searching values
        'context': {},  # context for searching values
    }

    def _setup_regular_base(self, model):
        pass

    @property
    def _related_domain(self):
        pass

    _related_context = property(attrgetter('context'))

    _description_relation = property(attrgetter('comodel_name'))
    _description_context = property(attrgetter('context'))

    def _description_domain(self, env):
        pass

    def null(self, record):
        pass

class Many2one(_Relational):
    type = 'many2one'
    column_type = ('int4', 'int4')
    _slots = {
        'ondelete': 'set null',  # what to do when value is deleted
        'auto_join': False,  # whether joins are generated upon search
        'delegate': False,  # whether self implements delegation
    }

    def __init__(self, comodel_name=Default, string=Default, **kwargs):
        super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)

    def _setup_attrs(self, model, name):
        pass

    def update_db(self, model, columns):
        pass

    def update_db_column(self, model, column):
        pass

    def update_db_foreign_key(self, model, column):
        pass

    def _update(self, records, value):
        pass

    def convert_to_column(self, value, record, values=None, validate=True):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_record(self, value, record):
        pass

    def convert_to_read(self, value, record, use_name_get=True):
        pass

    def convert_to_write(self, value, record):
        pass

    def convert_to_export(self, value, record):
        pass

    def convert_to_display_name(self, value, record):
        pass

    def convert_to_onchange(self, value, record, names):
        pass

class _RelationalMultiUpdate(object):
    __slots__ = ['record', 'field', 'value']

    def __init__(self, record, field, value):
        pass

    def __call__(self):
        pass

class _RelationalMulti(_Relational):
    _slots = {
        'context_dependent': True,  # depends on context (active_test)
    }

    def _update(self, records, value):
        pass

    def convert_to_cache(self, value, record, validate=True):
        pass

    def convert_to_record(self, value, record):
        pass

    def convert_to_read(self, value, record, use_name_get=True):
        pass

    def convert_to_write(self, value, record):
        pass

    def convert_to_onchange(self, value, record, names):
        pass

    def convert_to_export(self, value, record):
        pass

    def convert_to_display_name(self, value, record):
        pass

    def _compute_related(self, records):
        pass

    def _setup_regular_full(self, model):
        pass

class One2many(_RelationalMulti):
    type = 'many2many'
    _slots = {
        'relation': None,  # name of table
        'column1': None,  # column of table referring to model
        'column2': None,  # column of table referring to comodel
        'auto_join': False,  # whether joins are generated upon search
        'limit': None,  # optional limit to use upon read
    }

    def __init__(self, comodel_name=Default, inverse_name=Default, string=Default, **kwargs):
        super(One2many, self).__init__(
            comodel_name=comodel_name,
            inverse_name=inverse_name,
            string=string,
            **kwargs
        )

    def _setup_regular_base(self, model):
        pass

    def _setup_regular_full(self, model):
        pass

    def update_db(self, model, columns):
        pass

    def update_db_foreign_keys(self, model):
        pass

    def read(self, records):
        pass

    def create(self, record_values):
        pass

    def write(self, records, value):
        pass

class Many2many(_RelationalMulti):
    def __init__(self, comodel_name=Default, relation=Default, column1=Default,
                 column2=Default, string=Default, **kwargs):
        super(Many2many, self).__init__(
            comodel_name=comodel_name,
            relation=relation,
            column1=column1,
            column2=column2,
            string=string,
            **kwargs
        )

class Id(Field):
    type = 'integer'
    column_type = ('int4', 'int4')
    _slots = {
        'string': 'ID',
        'store': True,
        'readonly': True,
        'prefetch': False,
    }

    def update_db(self, model, columns):
        pass

    def __get__(self, record, owner):
        pass

    def __set__(self, record, value):
        pass

from odoo import SUPERUSER_ID
from .exceptions import AccessError, MissingError, UserError
from .models import check_pg_name, BaseModel, IdType